---
id: lerna
title: 准备工作
---

## lerna 介绍

[Lerna](https://lerna.js.org/) 是 monorepo 的管理模式中的一种工具，用于管理包含多个软件包（package）的 JavaScript 项目。

:::info
monorepo 是一种将多个 package 放在一个 repo(仓库)中的代码管理模式。
:::

- monorepo 解决的问题

  多个 repo 难以管理，编辑器需要打开多个项目;  
  某个模块升级，依赖改模块的其他模块需要手动升级，容易疏漏;  
  公用的 npm 包重复安装，占据大量硬盘容量，比如打包工具 webpack 会在每个项目中安装一次;  
  对新人友好，一句命令即可完成所有模块的依赖安装，且整个项目模块不用到各个仓库去找;  
  多 Package 本地 link  
  多 Package 依赖安装  
  多 Package 单元测试  
  多 Package 代码提交  
  多 Package 代码发布  
  发布时版本一致性  
  发布后相互依赖版本升级

- monorepo 带来的问题

  所有 package 代码集中在一个项目，单个项目体积较大;
  所有 package 代码对所有人可见，无法做权限管理;

- 优势
  大幅减少重复操作
  •提升操作的标准化
  Lerna 是架构优化的产物，它揭示了一个架构真理：项目复杂度提升后，就需要对要星頭参于行最雄教信：保均 anetkre
  优化的主要目标往往都是以效能为核心，

### 同类型的 monorepo 管理工具

|           工具           |                       备注                       |
| :----------------------: | :----------------------------------------------: |
|     Yarn Workspaces      |                                                  |
|           bit            |     [github](https://github.com/teambit/bit)     |
|            NX            |       [github](https://github.com/nrwl/nx)       |
|        Rush(微软)        | [github](https://github.com/microsoft/rushstack) |
|      Bazel（谷歌）       |                                                  |
| Buck 构建系统 (Facebook) |                                                  |
| Pants 构建系统(Twitter)  |                                                  |
|     Please 构建系统      |                                                  |
|           Oao            |                                                  |
|           Bolt           |                                                  |

### 常用 lerna 命令

|      命令       |                   作用                   |                                                         用法                                                         |
| :-------------: | :--------------------------------------: | :------------------------------------------------------------------------------------------------------------------: |
|   lerna init    | 初始化 lerna 项目，会创建一个 lerna.json | lerna add mocha --dev // 给所有 package 都安装 <br/>lerna add mocha package/core --dev //给 core 的 package 安装依赖 |
|  lerna create   |             创建一个 package             |                                                                                                                      |
|    lerna add    |          给 package 都安装依赖           |                                                                                                                      |
|  lerna publish  |                 发布项目                 |                                                                                                                      |
|   lerna link    |             链接互相引用的库             |                                                                                                                      |
|   lerna list    |            显示 package 列表             |                                                                                                                      |
|   lerna exec    |        在每个包目录下执行任意命令        |                                                                                                                      |
|    lerna run    |   执行每个包 package.json 中的脚本命令<br/ >lerna run --scope @xxx/utils test   |                                                                                                                      |
| lerna bootstrap |                 重装依赖                 |                                                                                                                      |

[更多 lerna 命令](https://lerna.js.org/docs/api-reference/commands)

### lerna 开发脚手架流程

1. 脚手架项目初始化:
   - 初始化 npm 项目
   - 安装 lerna
   - lerna init 初始化项目
2. 创建 package:
   - lerna create 创建 Pacakge
   - lerna add 安装依赖
   - lerna link 链接依赖
3. 脚手架开发和测试:
   - lerna exec 执行 shell 脚本
   - lerna run 执行 npm 命令
   - lerna clean 清空依赖
   - lerna bootstrap 重装依赖
4. 脚手架发布测试：
   - lerna version,bump version
   - lerna changed 查看版本以上的所有变更
   - lerna diff 查看 diff
   - lerna publish 项目发布

### lerna 实操

#### 使用 lerna 搭建脚手架框架

```c++
mkdir wdh-cli
npm init -y
npm install -g lerna (// 全局安装)
npm install lerna
lerna -v ( // 输出版本号说明安装成功)
lerna init (// 初始化lerna项目，会创建一个lerna.json)
// 经过上面init这一步，会初始化git仓库，再搞一个.gitignore 配置一些不用上传的目录
git remote xxx (// 添加远程仓库)
lerna create core (// 创建一个package)
lerna create utils(// 又创建了一个package)
lerna add (// 批量给两个package都安装依赖)
lerna publish (// 发布项目)
```

#### 核心操作

```js
lerna init  // 初始化lerna

```

创建 package

```
lerna create core
// 在这一步可以给package起一个别名 为了防止和npm上其它包重名 可以加个前缀@
// 这里要注意的是后面想成功发布这种带前缀的私包的话，需要先在npm上创建一个组织
// 举个栗子：如果你给这个包起的名字叫@tangmen-cli-dev/core
// 那你先要建一个叫tangmen-cli-dev的组织
```

安装依赖

```
lerna add mocha --dev (// 给所有package都安装)
lerna add mocha package/core --dev (给core这个package安装依赖)


```

清空依赖

```
lerna clean
// 手动从package的package.json中删除依赖
```

恢复依赖

```
lerna bootstrap

```

执行脚本

```
lerna exec -- rm -rf node_modules (// 在所有package中都执行)
lerna exec --scope @tangmen-cli-dev/core --rm -rf node_modules (// 在特定package中执行脚本)

```

执行 npm script 命令

```
lerna run test (// 在所有package中都执行)
lerna run --scope  @tangmen-cli-dev/core test (// 在特定package中执行脚本)

```

发布项目

```
lerna publish

```

- 发布时会自动执行：git add package-lock.json，所以 package-lock.json 不要加入.gitignore 文件
- 先创建远程仓库，并且同步一次 master 分支
- 执行 lerna publish 前先完成 npm login
- 如果发布的 npm 包名为：@xxx/yyy 的格式，需要先在 npm 注册名为：xxx 的 organization，否则可能会提交不成功
- 发布到 npm group 时默认为 private，所以我们需要手动在 package.json 中添加如下设置



### lerna 发布项目时遇到的坑

没有登录 npm及不能使用淘宝源的问题。  
tag 重复问题  
如果发布的是带前缀@的包，一定记得要先在 npm 上创建一个 Organization  
加了@前缀的包默认是 private 的，要在 package.json 中改变一下 publishConfig  
```
"publishConfig":{
  "access":"public"
}
```
package-lock.json 不能添加在.gitignore 中

### 理解 lerna 实现原理

通过 import-local 来优先调用本地 lerna 命令  
通过 Yargs 初始化脚手架，然后注册全局属性，再注册命令，最后通过 parse 方法解析参数  
lerna 命令注册时需要传入 build 和 handler 两个函数，build 用来注册命令专属的 options，handler 用来处理命令的业务逻辑  
lerna 通过配置 npm 本地依赖的方式进行本地开发，具体写法是在 package.json 中写入：file:your-locale-module-path,在 lerna publish 的时候会自动替换路径

## Yargs 简介

Yargs 通过解析参数和生成优雅的用户界面，帮助您构建交互式命令行工具。

### 演示

```js
#!/usr/bin/env node

const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const argv = yargs(hideBin(process.argv)).argv;

console.log(argv);

// 执行命令 wdh-cli --xxx   => { _: [], xxx: true, '$0': 'wdh-cli' }
// 执行命令 wdh-cli --xxx=abc   => { _: [], xxx: 'abc', '$0': 'wdh-cli' }
```

### 复杂演示

```js
#! /usr/bin/env node

const yargs = require("yargs");
const dedent = require("dedent");
const pkg = require("../package.json");

const cli = yargs();
const argv = process.argv.slice(2);
const context = {
  imoocVersion: pkg.version,
};
cli
  // 配置 第一行的使用提示
  .usage("Usage: imooc-cli-cai [command] <options>")
  // 配置 提示用户脚手架最少要接收一个命令
  .demandCommand(
    1,
    "A command is required. Pass --help to see all available commands and options."
  )
  // 配置 命令输入错误或者没有此命令的时候可以根据输入推荐合适的命令
  .recommendCommands()
  // 配置 命令错误时执行的方法
  .fail((err, msg) => {
    console.log(err);
  })
  // 配置 help和version的别名
  .alias("v", "version")
  .alias("h", "help")
  // 配置 终端宽度
  .wrap(cli.terminalWidth())
  // 配置 尾部的提示文字
  .epilogue(
    dedent`
      when a command fails,all logs are written to lerna-debug.log in the current working directory.

      For more information,find our manual at https://github.com/lerna/lerna
    `
  )
  // 配置 debug命令
  .options({
    debug: {
      type: "boolean",
      describe: "Bootstrap debug mode",
      alias: "d",
    },
  })
  // 配置 registry命令
  .option("registry", {
    type: "string",
    describe: "Define global registry",
    alias: "r",
  })
  // 配置 分组
  .group(["debug"], "Dev Options")
  .group(["registry"], "Extra Options")
  // 配置 命令，当执行 init [name] 命令的时候一系列的行为
  .command(
    "init [name]",
    "Do init a project",
    (yargs) => {
      yargs.option("name", {
        type: "string",
        describe: "Name of a project",
        alias: "n",
      });
    },
    (argv) => {
      console.log(argv);
    }
  )
  // 配置 命令的第二种方法
  .command({
    command: "list",
    aliases: ["ls", "ll", "la"],
    describe: "List local packages",
    builder: (yargs) => {},
    handler: (argv) => {
      console.log(argv);
    },
  })
  // 配置 严格模式，最后一行提示命令错误，如：无法识别的选项：lis
  .strict()
  // 解析参数
  .parse(argv, context);
```

### Yargs 常用 API

- Yargs.usage(提示脚手架用法)
- Yargs.strict(开启以后可以报错提示)
- Yargs.demandCommand(规定最少传几个 command)
- Yargs.recommendCommands(在输入错误 command 以后可以给你推荐最接近的正确的 command)
- Yargs.alias(起别名)
- Yargs.options(定义多个 option)
- Yargs.option(定义 option)
- Yargs.fail(错误处理方法)
- Yargs.group(分组)
- Yargs.wrap(命令行工具的宽度)
- Yargs.epilogue(命令行工具底部的提示)

### Yargs 开发流程

1. 脚手架初始化(将 process.argv 当参数传递给 Yargs())
2. 脚手架命令注册(Yargs.command)
3. 脚手架参数解析(Yargs.parse)

## commander

node.js 命令行界面的完整解决方案。(node 命令行框架)

```js
lerna commander colors packages/core
```

## semver

版本管理工具

```js
lerna add semver packages/core
```

## colors

提供丰富的文本颜色

```js
lerna add colors packages/core
```

## dotenv

从 .env 文件加载环境变量

```js
lerna add dotenv packages/core
```

## minimist

```js
lerna add minimist packages/core
```

## root-check

显示 npm 包储存位置的根目录（其实就是 node_modules 的所在位置）node

```js
lerna add root-check@1 packages/core
```

## user-home

此软件包已被弃用

```js
lerna add user-home packages/core
```

## 知识点

- [带@的npm包 ](https://www.npmjs.cn/misc/scope/)

## 扩展 (Node.js 模块路径解析流程)

Node.js 项目模块路径解析是通过 require.resolve 方法来实现的
require.resolve 就是通过 Module.\_resolveFileName 来实现的

require.resolve 实现原理：

1. Module.\_resolveFileName 核心的 3 个点:

- 判断是否为内置模块
- 通过 Module.\_resolveLookupPaths 生成 node_modules 可能存在的路径
- 通过 Module.\_findPath 查询模块的真实路径

2. Module.\_findPath 核心流程有 4 点:

- 查询缓存(将 request 和 paths 通过 x00 合并成 cacheKey)
- 遍历 paths,将 paths 与 request 组成文件路径 basePath
- 如果 basePath 存在则调用 fs.realPathSync 获取真实路径
- 将真实路径缓存到 Module.\_pathCache(key 就是前面生成的 cacheKey)

3. fs.realPathSync 核心流程有 3 点:

- 查询缓存（缓存的 key 为 p，也就是上面 Module.\_findPath 生成的路径）
- 从左往右遍历路径字符串遇到/时，拆分路径，判断该路径是否为软连接，如果是软连接则查询其真实链接，并生成新路径 P，然后继续往后遍历
- 遍历完成得到对应的真实路径，此时会将原始路径 original 作为 key,真实路径保存为 value 保存到缓存中

4. require.resolve.paths 等价于 Module.\_resolveLoopupPaths,该方法用于获取所有 node_modules 可能存在的路径

5. require.resolve.paths 的实现原理:

- 如果路径为根目录，直接返回['/node_modules']
- 否则将路径字符串从后往前遍历，遇见/时，拆分路经，在后面加上 node_modules,并传入一个 paths 数组，直至查询不到/后返回 paths 数组
